<?php

Raise::load('core.RaiseDataType');
Raise::load('core.RaiseVariable');
Raise::load('core.RaiseKeyValuePair');
Raise::load('core.RaiseConvert');
Raise::load('core.IRaiseComparator');
Raise::load('core.RaiseIterator');

/**
 * RaiseCollection class
 * Represents a higher level array with various functionalities
 *
 * Note that RaiseCollection is compatible with foreach() iterations like this:
 *     foreach(new RaiseCollection(array(5, 21, 3)) as $v){
 *
 *     }
 *
 * @author Sam-Mauris Yong <hellclanner at live dot com>
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @package Raise.Core.DataStructure.Collection
 * @since 1.2
 */
class RaiseCollection extends RaiseDataType implements ArrayAccess, IteratorAggregate {

    /**
     * The generic internal array variable
     * @var array
     */
    protected $_a = array();

    /**
     * Count caching
     * @var integer|boolean
     */
    private $count = false;

    /**
     * Create a RaiseCollection with a PHP generic array
     * @param mixed|array|RaiseCollection $a (optional) The array or RaiseCollection to create from
     * @param mixed,... (optional)
     */
    function __construct($a = null){
        if(func_num_args() == 1 && $a instanceof RaiseCollection){
            $this->_a = $a->_a;
        }elseif(func_num_args() == 1 && RaiseVariable::isType($a, RaiseVariable::TYPEARRAY)){
            $this->_a = $a;
        }elseif(func_num_args() > 0){
            $this->_a = func_get_args();
        }
    }

    /**
     * Add new element to the end of the array
     * @param string|mixed|RaiseKeyValuePair $keyOrValue Set the key if the second parameter $value exists, or otherwise append the value itself to the end of the array.
     * @param mixed $value (optional) The value of the new element. If this does not exist, the value of the new element is set as $keyOrValue
     */
    public function add(){
        $a = func_get_args();
        if(count($a) == 2)
        {
            if(!RaiseVariable::isType($a[0], array(RaiseVariable::TYPESTRING, RaiseVariable::TYPEINTEGER))){
                throw new RaiseInvalidArgumentException('The argument $keyOrValue must be string or integer when the second argument $value exists');
            }
            $this->_a[$a[0]] = $a[1];
        }
        elseif(count($a) == 1)
        {
            if($a[0] instanceof RaiseKeyValuePair){
                $this->_a[$a[0]->key()] = $a[0]->value();
            }else{
                $this->_a[] = $a[0];
            }
        }
        else
        {
            throw new RaiseInvalidArgumentException('The argument count is invalid. Should be add($key, $value) or add($value)');
        }
        $this->count = false;
    }

    /**
     * Selectively remove elements from the collection
     * @param RaiseCollection|array|string $key The key(s) of the elements to remove. If $key is an array or RaiseCollection, it will remove multiple elements. Otherwise only one element will be removed.
     */
    public function remove($key){
        if($key instanceof RaiseCollection || RaiseVariable::isType($key, RaiseVariable::TYPEARRAY)){
            if($key instanceof RaiseCollection){
                $key = $key->_a;
            }
            foreach($key as $k){
                if($this->keyExists($k)){
                    unset($this->_a[$k]);
                }
            }
        }else{
            if($this->keyExists($key)){
                unset($this->_a[$key]);
            }
        }
        $this->count = false;
    }

    /**
     * Clear all elements off the collection
     * and reset to an empty collection
     */
    public function clear(){
        $this->count = false;
        $this->_a = array();
    }

    /**
     * Return a collection of keys of this array
     * @return RaiseCollection
     */
    public function keys(){
        return new RaiseCollection(array_keys($this->_a));
    }

    /**
     * Return a collection of values of this array
     * @return RaiseCollection
     */
    public function values(){
        return new RaiseCollection(array_values($this->_a));
    }

    /**
     * Check whether a particular key exists in the collection
     * @param integer|string $key The key to check
     * @return boolean
     */
    public function keyExists($key){
        return array_key_exists($key, $this->_a);
    }

    /**
     * Check whether a particular value is in the array
     * @param mixed $value The value to check
     * @return boolean
     */
    public function valueExists($value){
        return in_array($value, $this->_a, true);
    }

    /**
     * Check if a specific key value combination exists in the collection
     * @param integer|string $key The key to check
     * @param mixed $value The value to check
     * @return boolean
     */
    public function keyValueExists($key, $value){
        if($this->keyExists($key)){
            if($this->get($key) === $value){
                return true;
            }
        }
        return false;
    }

    /**
     * Get the value of an element with a key
     * @param string|integer $key The key of the element
     * @return mixed The value of the element. Note that if the item does not exist, the method returns NULL.
     */
    public function get($key){
        if($this->keyExists($key)){
            $i = $this->_a[$key];
            if(RaiseVariable::isType($i, RaiseVariable::TYPEARRAY)){
                $i = new RaiseCollection($i);
            }
            return $i;
        }
        else
        {
            return null;
        }
    }

    /**
     * Copy a segment of the array out to a new RaiseCollection
     * @param int $start Start position of the collection to start
     * @param int $count (optional) Number of elements from the Start position $start to retrieve, If not set, the segment will consist of elements from $start to the end of the collection.
     * @return RaiseCollection
     */
    public function segment($start, $count = false){
        $tc = $this->count();
        if($start > $tc){
            throw new RaiseInvalidArgumentException('Argument $start has exceeded the total number of elements in the collection');
        }
        $a = $this->_a;
        reset($a);
        $i = -1;
        if($count === false){
            $count = $tc - $start;
        }
        while(++$i < $start){
            next($a);
        }
        $i = -1;
        $r = new RaiseCollection();
        while($i++ < $count - 1){
            $t = each($a);
            if($t === false){
                break;
            }
            $r->add($t['key'], $t['value']);
        }
        return $r;
    }

    /**
     * Return the internal PHP generic array
     * @return array
     */
    public function toArray(){
        return $this->_a;
    }

    /**
     * Converts the collection to JSON string
     * @return string
     */
    public function toJSON(){
        return RaiseConvert::toJSONString($this->_a);
    }

    /**
     * Creates a new RaiseCollection from a JSON string
     * @param string $j The JSON string
     * @return RaiseCollection
     * @static
     */
    public static function fromJSON($j){
        $a = new RaiseCollection(RaiseConvert::fromJSONString($this->_a));
        return $a;
    }

    /**
     * Return the value of the first element
     * @return mixed
     */
    public function first(){
        $a = $this->_a;
        if($this->count() == 0){
            return null;
        }
        return reset($a);
    }

    /**
     * Return the value of the last element
     * @return mixed
     */
    public function last(){
        $a = $this->_a;
        if($this->count() == 0){
            return null;
        }
        return end($a);
    }

    /**
     * Get the first key of the first occurance of the $o value
     * @param mixed $o Value of the key to search
     * @return mixed
     */
    public function indexOf($o){
        $index = array_search($o, $this->_a, true);
        if($index === false){
            $index = null;
        }
        return $index;
    }

    /**
     * Retrieve all the keys of a specific value
     * @param mixed $o Value of the keys to return
     * @return RaiseCollection
     */
    public function indexesOf($o){
        $i = new RaiseCollection();
        foreach($this->_a as $k => $v){
            if($v == $o){
                $i->add($k);
            }
        }
        return $i;
    }

    /**
     * Walk through the collection with a function
     * @param callback $func The function to walk the collection
     * @param boolean $recursive (optional) Sets whether the function should parse the collection recursively
     * @link http://php.net/array-walk
     */
    public function walk($func, $recursive = false){
        if(!RaiseVariable::isCallable($func)){
            throw new RaiseInvalidArgumentException('The argument $func is not a valid callable function / class method.');
        }
        if($recursive){
            array_walk_recursive($this->_a, $func);
        }else{
            array_walk($this->_a, $func);
        }
    }

    /**
     * Shuffle the collection
     * @link http://php.net/shuffle
     */
    public function shuffle(){
        shuffle($this->_a);
    }

    /**
     * Return a random key/value pair from the collection
     * @return RaiseKeyValuePair
     */
    public function random(){
        $t = array_rand($this->_a);
        $r = new RaiseKeyValuePair($t, $this->get($t));
        return $r;
    }

    /**
     * Get the number of element in the collection
     * Note that count is cached in RaiseCollection::$count
     * Clearing cache is done after CUD operations
     * @return integer
     * @link http://php.net/count
     */
    public function count(){
        if($this->count === false){
            return $this->count = count($this->_a);
        }else{
            return $this->count;
        }
    }

    /**
     * Query whether the collection is sequential (non-associative) or not.
     * @return boolean
     */
    public function isSequential(){
        return array_values($this->_a) === $this->_a;
    }


    /**
     * Query whether the collection is associative or not.
     * @return boolean
     */
    public function isAssociative(){
        return !$this->isSequential();
    }

    /**
     * Reverse the entire collection
     * @link http://www.php.net/array-reverse
     */
    public function reverse(){
        $this->_a = array_reverse($this->_a);
    }

    /**
     * Prepend the collection with another RaiseCollection or array
     * @param RaiseCollection|array $a
     */
    public function prepend($a){
        if($a instanceof RaiseCollection){
            $this->_a = array_merge($a->_a, $this->_a);
        }else{
            $this->_a = array_merge($a, $this->_a);
        }
        $this->count = false;
    }

    /**
     * Join the elements in the array to form a single string
     * @param string $deliminator (optional) If set, all elements will be separated in the string by this deliminator
     * @return string
     * @link http://www.php.net/implode
     */
    public function join($deliminator = null){
        if(func_num_args() == 1){
            return implode($deliminator, $this->_a);
        }else{
            return implode($this->_a);
        }
    }

    /**
     * Append the collection with another RaiseCollection or array
     * @param RaiseCollection|array $a
     */
    public function append($a){
        if($a instanceof RaiseCollection){
            $this->_a = array_merge($this->_a, $a->_a);
        }elseif(is_array($a)){
            $this->_a = array_merge($this->_a, $a);
        }else{
            throw new RaiseInvalidArgumentException('');
        }
        $this->count = false;
    }

    /**
     * Exchanges all keys with their associated values in this collection
     * @link http://www.php.net/flip
     */
    public function flip(){
        $this->_a = array_flip($this->_a);
    }

    /**
     * Get the intersection between this collection and another
     * @param RaiseCollection $a The collection to compare against
     * @param boolean $keys (optional) If set to TRUE, the keys will be considered in the comparison
     * @return RaiseCollection
     */
    public function intersect($a, $keys = null){
        $r = new RaiseCollection();
        if($a instanceof RaiseCollection){
            if($keys){
                $r->_a = array_intersect_assoc($this->_a, $a->_a);
            }else{
                $r->_a = array_intersect($this->_a, $a->_a);
            }
        }else{
            if($keys){
                $r->_a = array_intersect_assoc($this->_a, $a);
            }else{
                $r->_a = array_intersect($this->_a, $a);
            }
        }
        return $r;
    }

    /**
     * Get the difference between this collection and another
     * @param RaiseCollection $a The collection to compare against
     * @param boolean $keys (optional) If set to TRUE, the keys will be considered in the comparison
     * @return RaiseCollection
     */
    public function difference($a, $keys = null){
        $r = new RaiseCollection();
        if($a instanceof RaiseCollection){
            if($keys){
                $r->_a = array_diff_assoc($this->_a, $a->_a);
            }else{
                $r->_a = array_diff($this->_a, $a->_a);
            }
        }else{
            if($keys){
                $r->_a = array_diff_assoc($this->_a, $a);
            }else{
                $r->_a = array_diff($this->_a, $a);
            }
        }
        return $r;
    }

    /**
     * Sort the collection using a comparator
     * Note that when the collection is sequential, the association with the index is not maintained.
     * If the collection is associative, the index association is maintained as it is.
     * @param IRaiseComparator $comparator
     */
    public function sort($comparator){
        if(!($comparator instanceof IRaiseComparator)){
            throw new RaiseInvalidArgumentException('Sorting collection requires a IRaiseComparator to assist sorting.');
        }
        if($this->isSequential()){
            usort($this->_a, array($comparator, 'compare'));
        }else{
            uasort($this->_a, array($comparator, 'compare'));
        }
    }

    /**
     * Get the RaiseIterator for this RaiseCollection
     * @return RaiseIterator
     */
    public function iterator(){
        return new RaiseIterator($this->_a);
    }

    public function equals($o) {
        if($o instanceof RaiseCollection){
            if($o->count() == $this->count()){
                if($o->_a == $this->_a){
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Array Access at work
     * @param string|mixed $keyOrValue
     * @param mixed $value
     * @ignore
     */
    public function offsetSet($keyOrValue, $value) {
        if ($keyOrValue == null) {
            $this->add($keyOrValue);
        } else {
            $this->add($keyOrValue, $value);
        }
    }

    /**
     * Array Access at work
     * @param string $offset
     * @return boolean
     * @ignore
     */
    public function offsetExists($offset) {
        return $this->keyExists($offset);
    }

    /**
     * Array Access at work
     * @param string $offset
     * @ignore
     */
    public function offsetUnset($offset) {
        $this->remove($offset);
    }

    /**
     * Array Access at work
     * @param string $offset
     * @return mixed
     * @ignore
     */
    public function offsetGet($offset) {
        return $this->get($offset);
    }

    /**
     * For traversing of values using foreach.
     * @return ArrayIterator
     * @ignore
     */
    public function getIterator() {
       return new ArrayIterator($this->_a);
    }

}
